注:原书作者 Steven F. Lott,原书名为 Mastering Object-oriented Python
__hash__()
方法
内置hash()
函数会调用给定对象的__hash__()
方法。这里hash就是将(可能是复杂的)值缩减为小整数值的计算。理想情况下,一个hash值反映了源值的所有信息。还有一些hash计算经常用于加密,生成非常大的值。
Python包含两个hash库。在hashlib
模块中有高品质加密hash函数。zlib
模块有两个高速hash函数:adler32()
和crc32()
。对于相对简单的值,我们不使用这些。而对于大型、复杂的值,使用这些算法会有很大帮助。
hash()
函数(和相关的__hash__()
方法)用于创建集合中使用的小整数key,如下集合:set
、frozenset
和dict
。这些集合使用不可变对象的hash值来快速定位对象。
在这里不变性是很重要的,我们会多次提到它。不可变对象不会改变它们的状态。例如,数字3并没有改变状态,它总是3。更复杂的对象也是一样的,可以有一个不变的状态。Python字符串是不可变的,这样它们可以用来映射作集合的key。
默认的__hash__()
继承自对象本身,返回一个基于对象的内部ID值。这个值可以通过id()
函数看到,如下:
>>> x = object()
>>> hash(x)
269741571
>>> id(x)
4315865136
>>> id(x) / 16
269741571.0
由此,我们可以看到在作者的系统中,hash值就是对象的id / 16
。这一细节针对不同平台可能会有所不同。例如,CPython使用可移植的C库,Jython依赖于Java JVM。
至关重要的是,内部ID和默认__hash__()
方法间有一种强联系。这意味着每个对象默认是可以hash且完全不同的,即使它们似乎相同。
如果我们想将有相同值的不同对象合并到单个可hash对象中,我们需要修改这个。在下一节中,我们将看一个示例,该示例一个卡片的两个实例被视为是同一个对象。
1. 判断什么需要hash
不是每一个对象都需要提供一个hash值。具体地说,如果我们创建一个有状态、可变对象的类,该类万万不能返回hash值。__hash__
应该定义为None
。
另一方面,不可变对象返回一个hash值,这样对象就可用作字典中的key或集合中的一员。在这种情况下,hash值需要用并行的方式检测相等性。对象有不同的hash值但被看作相等的对象是糟糕的。相反的,对象具有相同hash值,实际上不相等是可以接受的。
我们在比较运算符中看到的__eq__()
方法与hash关系密切。
有三种级别的等式比较:
相同的hash值:这意味着两个对象可能是相等的。该hash值为我们提供了一个快速检查对象相等的可能性。如果hash值是不同的,两个对象不可能是相等的,他们也不可能是相同的对象。
等号比较:这意味着hash值也一定相等。这是
==
操作符的定义。对象可能是相同的对象。相同的IDD:这意味着他们是同一个对象。进行了等号比较且有相同的hash值。这是
is
操作符的定义。
Hash的基本规律(FLH)是:对象等号比较必须具有相同的hash值。
在相等性检测中我们能想到的第一步是hash比较。
然而,反过来是不正确的。对象可以有相同的hash值但比较是不相等的。在创建集合或字典时导致一些预计的处理开销是正当的。我们不能确切的从更大的数据结构创建不同的64位hash值。将有不相等的对象被简化为一致相等的hash值。
在使用集合和字典时比较hash值是一个预期的开销,它们是同时发生的。这些集合有内部的算法在hash冲突时会使用替换位置进行处理。
有三个用例通过__eq__()
和__hash__()
方法定义相等性检测和hash值:
-
不可变对象:对于有些无状态对象,例如
tuples
、namedtuples
、frozensets
这些不能被更新的类型。我们有两个选择:不定义
__hash__()
和__eq__()
。这意味着什么都不做,使用继承的定义。在这种情况下__hash__()
返回一个简单的函数对象的ID值,然后__eq__()
比较ID值。默认的相等性检测有时是违反直觉的。我们的应用程序可能需要两个Card(1, Clubs)
实例检测相等性和计算相同的hash,默认情况下是不会发生这种情况的。定义
__hash__()
和__eq__()
。请注意,我们将为不可变对象定义以上两个。
-
可变对象:这些是有状态的对象,可以进行内部修改。我们有一个选择:
定义
__eq__()
,但__hash__()
设置为None
。这些不能被用作dict
中的key或set
中的项目。
请注意,有一个额外可能的组合:定义__hash__()
但对__eq__()
使用一个默认的定义。这其实是浪费时间,作为默认的__eq__()
方法其实和is
操作符是一样的。默认的__hash__()
方法会为相同的行为编写更少的代码。
我们可以详细的看看这三种情况。
2. 为不可变对象继承定义
让我们看看默认定义操作。下面是一个简单的类层次结构,使用默认的__hash__()
和__eq__()
定义:
class Card:
insure= False
def __init__(self, rank, suit, hard, soft):
self.rank = rank
self.suit = suit
self.hard = hard
self.soft = soft
def __repr__(self):
return "{__class__.__name__}(suit={suit!r}, rank={rank!r})"
.format(__class__=self.__class__, **self.__dict__)
def __str__(self):
return "{rank}{suit}".format(**self.__dict__)
class NumberCard(Card):
def __init__(self, rank, suit):
super().__init__(str(rank), suit, rank, rank)
class AceCard(Card):
def __init__(self, rank, suit):
super().__init__("A", suit, 1, 11)
class FaceCard(Card):
def __init__(self, rank, suit):
super().__init__({11: 'J', 12: 'Q', 13: 'K'}[rank], suit, 10, 10)
这是一个不可变对象的类层次结构。我们还没有实现特殊方法防止属性更新。在下一章我们将看看属性访问。
当我们使用这个类层次结构时,看看会发生什么:
>>> c1 = AceCard(1, '♣')
>>> c2 = AceCard(1, '♣')
我们定义的两个相同的Card
实例。我们可以检查id()
的值,如下代码片段所示:
>>> print(id(c1), id(c2))
4302577232 4302576976
他们有不同的id()
号,不同的对象。这符合我们的预期。
我们可以使用is
操作符来检查它们是否一样,如下代码片段所示:
>>> c1 is c2
False
“is
测试”是基于id()
的数字,它告诉我们,它们确实是独立的对象。
我们可以看到,它们的hash值是不同的:
>>> print(hash(c1), hash(c2))
268911077 268911061
这些hash值直接来自id()
值。这是我们期望继承的方法。在这个实现中,我们可以从id()
函数中计算出hash值,如下代码片段所示:
>>> id(c1) / 16
268911077.0
>>> id(c2) / 16
268911061.0
hash值是不同的,它们之间的比较必须不相等。这符合hash的定义和相等性定义。然而,这违背了我们对这个类的期望。下面是一个相等性检查:
>>> print(c1 == c2)
False
我们使用相同的参数创建了它们。它们比较后不相等。在某些应用程序中,这样不好。例如,当处理牌的时候累加计数,我们不想给一张牌做6个计数因为使用的是6副牌牌盒。
我们可以看到,他们是不可变对象,我们可以把它们放在一个集合里:
>>> print(set([c1, c2]))
{AceCard(suit='♣', rank=1), AceCard(suit='♣', rank=1)}
这是标准库参考文档中记录的行为。默认情况下,我们会得到一个基于对象ID的__hash__()
方法,这样每个实例都唯一出现。然而,这并不总是我们想要的。
3. 覆写不可变对象的定义
下面是一个简单的类层次结构,它为我们提供了__hash__()
和__eq__()
的定义:
class Card2:
insure = False
def __init__(self, rank, suit, hard, soft):
self.rank = rank
self.suit = suit
self.hard = hard
self.soft = soft
def __repr__(self):
return "{__class__.__name__}(suit={suit!r}, rank={rank!r})".
format(__class__=self.__class__, **self.__dict__)
def __str__(self):
return "{rank}{suit}".format(**self.__dict__)
def __eq__(self, other):
return self.suit == other.suit and self.rank == other.rank
def __hash__(self):
return hash(self.suit) ^ hash(self.rank)
class AceCard2(Card2):
insure = True
def __init__(self, rank, suit):
super().__init__("A", suit, 1, 11)
原则上这个对象是不可变的。还没有正式的机制来让它不可变。关于这个机制我们将在第3章《属性访问、属性和描述符》中看看如何防止属性值变化。
同时,注意前面的代码省略了的两个子类,从前面的示例来看并没有显著的改变。
__eq__()
方法函数比较这两个基本值:suit
和rank
。它不比较派生自rank
的hard
值和soft
值。
21点的规则使这个定义有点可疑。花色在21点中实际上并不重要。我们只是比较牌值吗?我们是否应该定义一个额外的方法,而不是仅仅比较牌值?或者,我们应该依靠应用程序比较牌值的正确性?对于这些问题没有最好的回答,只是做好一个权衡。
__hash__()
方法函数计算的位模式使用两个值作为基础进行hash,然后对hash值进行异或计算。使用^
操作符是一种应急的hash方法,很有用。对于更大、更复杂的对象,使用更复杂的hash会更合适。在构造某个东东之前使用ziplib
会有bug哦。
让我们来看看这些类对象的行为。我们期望它们比较是相等的且能够在集合和字典中正常使用。这里有两个对象:
>>> c1 = AceCard2(1, '♣')
>>> c2 = AceCard2(1, '♣')
我们定义的两个实例似乎是相同的牌。我们可以检查ID值,以确保他们是不同的对象:
>>> print(id(c1), id(c2))
4302577040 4302577296
>>> print(c1 is c2)
False
这些有不同的id()
数字。当我们通过is
操作符检测,我们看到它们是截然不同的。
让我们来比较一下hash值:
>>> print(hash(c1), hash(c2))
1259258073890 1259258073890
hash值是相同的。这意味着他们可能是相等的。
等号操作符告诉我们,他们是相等的
>>> print(c1 == c2)
True
它们是不可变的,我们可以把它们放到一个集合中,如下所示:
>>> print(set([c1, c2]))
{AceCard2(suit='♣', rank='A')}
对于复杂的不可变对象是符合我们预期的。我们必须覆盖这两个特殊方法获得一致的、有意义的结果。
4. 覆写可变对象的定义
这个例子将继续使用Cards
类。可变的牌是很奇怪的想法,甚至是错误的。然而,我们想小小调整一下前面的例子。
以下是一个类层次结构,为我们提供了适合可变对象的__hash__()
和__eq__()
的定义:
class Card3:
insure = False
def __init__(self, rank, suit, hard, soft):
self.rank = rank
self.suit = suit
self.hard = hard
self.soft = soft
def __repr__(self):
return "{__class__.__name__}(suit={suit!r}, rank={rank!r})".
format(__class__=self.__class__, **self.__dict__)
def __str__(self):
return "{rank}{suit}".format(**self.__dict__)
def __eq__(self, other):
return self.suit == other.suit and self.rank == other.rank
# and self.hard == other.hard and self.soft == other.soft
__hash__ = None
class AceCard3(Card3):
insure= True
def __init__(self, rank, suit):
super().__init__("A", suit, 1, 11)
让我们来看看这些类对象的行为。我们期望它们比较是相等的,但是在集合和字典中完全不起作用。我们创建如下两个对象:
>>> c1 = AceCard3(1, '♣')
>>> c2 = AceCard3(1, '♣')
我们定义的两个实例似乎是相同的牌。我们可以检查ID值,以确保他们是不同的对象:
>>> print(id(c1), id(c2))
4302577040 4302577296
如果我们尝试获取hash值,毫无意外,我们将会看到如下情形:
>>> print(hash(c1), hash(c2))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'AceCard3'
__hash__
被设置为None
,这些Card3
对象不能被hash,不能为hash()
函数提供值。和我们预期的是一样的。
我们可以执行相等性比较,如下代码片段所示:
>>> print(c1 == c2)
True
相等性测试工作正常,才能很好的让我们比较牌。它们只是不能被插入到集合或用作字典的key。
我们试试会发生什么:
>>> print(set([c1, c2]))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'AceCard3'
当试图把这些放到集合中,我们会得到这样一个异常。
显然,这不是一个正确的定义,在现实生活中和牌一样是不可变对象。这种风格的定义更适合有状态的对象,如Hand
,它的内容总是在变化的。我们将通过第二个示例为您提供一个有状态的对象在接下来的章节。
5. 从可变手牌变为冻结手牌
如果我们想对具体的Hand
实例进行统计分析,我们可能需要创建一个字典来映射Hand
实例到计数中。我们不能用一个可变Hand
类作为一个映射的key。然而,我们可以并行的设计set
和frozenset
并且创建两个类:Hand
和FrozenHand
。这允许我们能通过FrozenHand
类“冻结”Hand
类;冻结版本是不可变的,可以作为一个字典的key。
下面是一个简单的Hand
定义:
class Hand:
def __init__(self, dealer_card, *cards):
self.dealer_card = dealer_card
self.cards = list(cards)
def __str__(self):
return ", ".join(map(str, self.cards))
def __repr__(self):
return "{__class__.__name__}({dealer_card!r}, {_cards_str})"
.format(__class__=self.__class__, _cards_str=", "
.join(map(repr, self.cards)), **self.__dict__)
def __eq__(self, other):
return self.cards == other.cards and self.dealer_card == other.dealer_card
__hash__ = None
这是一个可变对象(__hash__
是None
),它有一个恰当的相等性检测来比较两副手牌。
下面是关于Hand
的一个“冻结”版本:
import sys
class FrozenHand(Hand):
def __init__(self, *args, **kw):
if len(args) == 1 and isinstance(args[0], Hand):
# Clone a hand
other = args[0]
self.dealer_card = other.dealer_card
self.cards = other.cards
else:
# Build a fresh hand
super().__init__(*args, **kw)
def __hash__(self):
h = 0
for c in self.cards:
h = (h + hash(c)) % sys.hash_info.modulus
return h
冻结版本有一个构造函数,将从另一个Hand
类构建一个Hand
类。它定义了一个__hash__()
方法,计算牌的hash值的总和,这个值受sys.hash_info.modules
限制。大多数情况,这种基于模块的计算,在计算复合对象hash时效果相当好。
我们现在可以使用这些类进行操作,如下代码片段所示:
stats = defaultdict(int)
d = Deck()
h = Hand(d.pop(), d.pop(), d.pop())
h_f = FrozenHand(h)
stats[h_f] += 1
我们需要初始化统计字典——stats
为defaultdict
字典,可以收集整型计数。为此我们可以使用一个collections.Counter
对象。
通过冻结Hand
类,我们可以把它作为一个字典的key,收集每副手牌计数的问题就可以解决了。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。